<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2017/11/2 0002
 * Time: 9:02
 */

class AlipayTradeService
{
    /**
     * @var string 支付网关
     */
    public $gateway = 'https://mapi.alipay.com/gateway.do?';
    /**
     * @var string HTTPS 方式校验通知的支付宝网关
     */
    public $https_verify_url = 'https://mapi.alipay.com/gateway.do?service=notify_verify&';
    /**
     * @var string HTTP 方式校验通知的支付宝网关
     */
    public $http_verify_url = 'http://notify.alipay.com/trade/notify_query.do?';


    protected $config = array();

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    /**
     * 生成签名结果
     * @param array $para_sort 已排序要签名的数组
     * @return string 签名结果字符串
     */
    protected function buildRequestMysign($para_sort)
    {
        //把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = $this->createLinkString($para_sort);
        $mysign = '';
        switch (strtoupper(trim($this->config['sign_type']))) {
            case "MD5" :
                $mysign = md5($prestr . $this->config['key']);
                break;
            case "RSA":{
                $private_key = $this->config['rsa_private_cert'];
                $search = array("-----BEGIN RSA PRIVATE KEY-----","-----END RSA PRIVATE KEY-----","\n");

                $private_key=str_replace($search,"",$private_key);

                $private_key="-----BEGIN RSA PRIVATE KEY-----".PHP_EOL .wordwrap($private_key, 64, "\n", true). PHP_EOL."-----END RSA PRIVATE KEY-----";

                $res = openssl_get_privatekey($private_key);
                if(!$res){
                    return false;
                }
                openssl_sign($prestr, $mysign, $res);
                openssl_free_key($res);
                //base64编码
                $mysign = base64_encode($mysign);
                break;
            }
            default :

                $mysign = "";
        }

        return $mysign;
    }

    /**
     * 生成要请求给支付宝的参数数组
     * @param array $para_temp 请求前的参数数组
     * @return array 要请求的参数数组
     */
    protected function buildRequestParams($para_temp)
    {
        //除去待签名参数数组中的空值和签名参数
        $para_filter = $this->paraFilter($para_temp);

        //对待签名参数数组排序
        $para_sort = $this->argSort($para_filter);

        //生成签名结果
        $mysign = $this->buildRequestMysign($para_sort);
        if(empty($mysign)){
            throw new Exception('生成签名失败');
        }

        //签名结果与签名方式加入请求提交参数组中
        $para_sort['sign'] = $mysign;
        $para_sort['sign_type'] = strtoupper(trim($this->config['sign_type']));

        return $para_sort;
    }

    /**
     * 生成要请求给支付宝的参数数组
     * @param array $para_temp 请求前的参数数组
     * @return string 要请求的参数数组字符串
     */
    protected function buildRequestParamsToString($para_temp)
    {
        //待请求参数数组
        $para = $this->buildRequestParams($para_temp);

        //把参数组中所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串，并对字符串做urlencode编码
        $request_data = http_build_query($para);

        return $request_data;
    }

    /**
     * 获取电脑端支付参数
     * @param AlipayTradeBuilder $builder
     * @param string $method
     * @return string
     */
    public function pageExecute($builder,$method = 'post')
    {
        $params = $builder->getBizContent();
        $params['service'] = $builder->getServiceName();
        $params['partner'] = $this->config['partner'];
        $params['_input_charset'] = strtoupper($this->config['input_charset']);
        $params['sign_type'] = $this->config['sign_type'];

        //print_r($params);exit;
        return $this->buildRequestForm($params,$method);
    }
    /**
     * 建立请求，以表单HTML形式构造（默认）
     * @param array $para_temp 请求参数数组
     * @param string $method 提交方式。两个值可选：post、get
     * @param string $button_name 确认按钮显示文字
     * @return  string 提交表单HTML文本
     */
    public function buildRequestForm($para_temp, $method = 'post', $button_name = '确定')
    {
        //待请求参数数组
        $para = $this->buildRequestParams($para_temp);

        if ($method == 'post') {

            $sHtml = "<form id='alipaysubmit' name='alipaysubmit' action='" . $this->gateway . "_input_charset=" . trim(strtolower($this->config['input_charset'])) . "' method='" . $method . "'>";
            foreach ($para as $key => $val) {
                $sHtml .= "<input type='hidden' name='" . $key . "' value='" . $val . "'/>";
            }

            //submit按钮控件请不要含有name属性
            $sHtml = $sHtml . "<input type='submit' value='" . $button_name . "'></form>";

            $sHtml = $sHtml . "<script>document.forms['alipaysubmit'].submit();</script>";
        } elseif ($method == 'raw') {
            $sHtml = $this->createLinkString($para);
        } elseif ($method == 'encode') {
            $sHtml = $this->createLinkStringUrlEncode($para);
        } elseif ($method == 'app') {
//            $sHtml = '';
//            foreach ($para as $key => $val) {
//                $sHtml .= $key . '="' . $val . '"&';
//            }
//            $sHtml = rtrim($sHtml, '&');
//            //如果存在转义字符，那么去掉转义
//            if (get_magic_quotes_gpc()) {
//                $sHtml = stripslashes($sHtml);
//            }

            $sHtml =  $this->createLinkStringUrlEncode($para);

        } else {
            $query = $this->createLinkStringUrlEncode($para);
            $sHtml = $this->gateway . $query;
        }
        return $sHtml;
    }

    /**
     * 建立请求，以模拟远程HTTP的POST请求方式构造并获取支付宝的处理结果
     * @param AlipayTradeBuilder $builder 请求参数数组
     * @param string $method
     * @return string 支付宝处理结果
     */
    public function execute($builder,$method = 'post')
    {
        $params = $builder->getBizContent();
        $params['service'] = $builder->getServiceName();
        $params['partner'] = $this->config['partner'];
        $params['_input_charset'] = strtoupper($this->config['input_charset']);
        $params['sign_type'] = $this->config['sign_type'];

        //待请求参数数组字符串
        $request_data = $this->buildRequestParams($params);

        if($method == 'post'){
            //远程获取数据
            $sResult = $this->getHttpResponsePost($this->gateway, $this->config['cacert_path'],$request_data,trim(strtolower($this->config['input_charset'])));
        }else{

            $url = $this->gateway . http_build_query($request_data);
            // exit($url);
            $sResult = $this->getHttpResponseGet($url,$this->config['cacert_path']);
        }


        return $sResult;
    }

    /**
     * 建立请求，以模拟远程HTTP的POST请求方式构造并获取支付宝的处理结果，带文件上传功能
     * @param array $para_temp 请求参数数组
     * @param string $file_para_name 文件类型的参数名
     * @param string $file_name 文件完整绝对路径
     * @return string 支付宝返回处理结果
     */
    public  function buildRequestHttpInFile($para_temp, $file_para_name, $file_name)
    {

        //待请求参数数组
        $para = $this->buildRequestParams($para_temp);
        $para[$file_para_name] = "@".$file_name;

        //远程获取数据
        $sResult = $this->getHttpResponsePost($this->gateway, $this->config['cacert_path'],$para,trim(strtolower($this->config['input_charset'])));

        return $sResult;
    }

    /**
     * 用于防钓鱼，调用接口query_timestamp来获取时间戳的处理函数
     * 注意：该功能PHP5环境及以上支持，因此必须服务器、本地电脑中装有支持DOMDocument、SSL的PHP配置环境。建议本地调试时使用PHP开发软件
     * @return string 时间戳字符串
     */
    public function queryTimestamp()
    {
        $url = $this->gateway."service=query_timestamp&partner=".trim(strtolower($this->config['partner']))."&_input_charset=".trim(strtolower($this->config['input_charset']));

        $doc = new \DOMDocument();
        $doc->load($url);
        $itemEncrypt_key = $doc->getElementsByTagName( "encrypt_key" );
        $encrypt_key = $itemEncrypt_key->item(0)->nodeValue;

        return $encrypt_key;
    }

    /**
     * 获取当前系统时间戳
     * @return float
     */
    public function getMillisecond()
    {
        list($t1, $t2) = explode(' ', microtime());
        return (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000);
    }
    /**
     * 针对notify_url验证消息是否是支付宝发出的合法消息
     * @param array $params
     * @return bool 验证结果
     */
    public function verifyNotify($params){
        if(empty($params)) {//判断POST来的数组是否为空
            return false;
        }
        else {
            //验证MD5的结果
            $isSign = $this->getSignVeryfy($params, $params["sign"]);

            //获取支付宝远程服务器ATN结果（验证是否是支付宝发来的消息）
            $responseTxt = 'false';
            if (! empty($params["notify_id"])) {
                $responseTxt = $this->getResponse($params["notify_id"]);
            }

            //写日志记录
            //if ($isSign) {
            //	$isSignStr = 'true';
            //}
            //else {
            //	$isSignStr = 'false';
            //}
            //$log_text = "responseTxt=".$responseTxt."\n notify_url_log:isSign=".$isSignStr.",";
            //$log_text = $log_text.createLinkString($_POST);
            //logResult($log_text);


            //验证
            //$responsetTxt的结果不是true，与服务器设置问题、合作身份者ID、notify_id一分钟失效有关
            //isSign的结果不是true，与安全校验码、请求时的参数格式（如：带自定义参数等）、编码格式有关
            if (preg_match("/true$/i",$responseTxt) && $isSign) {
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * 针对return_url验证消息是否是支付宝发出的合法消息
     * @return bool 验证结果
     */
    public function verifyReturn()
    {
        if(empty($_GET)) {//判断POST来的数组是否为空
            return false;
        }
        else {
            //生成签名结果
            $isSign = $this->getSignVeryfy($_GET, $_GET["sign"]);
            return $isSign;
        }
    }

    /**
     * 获取返回时的签名验证结果
     * @param array $para_temp 通知返回来的参数数组
     * @param string $sign 返回的签名结果
     * @return bool 签名验证结果
     */
    protected function getSignVeryfy($para_temp, $sign)
    {
        //除去待签名参数数组中的空值和签名参数
        $para_filter = $this->paraFilter($para_temp);

        //对待签名参数数组排序
        $para_sort = $this->argSort($para_filter);

        //把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
        $prestr = $this->createLinkstring($para_sort);

        switch (strtoupper(trim($this->config['sign_type']))) {
            case "MD5" :
                $isSgin = md5($prestr. $this->config['key']) == $sign;
                break;
            default :
                $isSgin = false;
        }

        return $isSgin;
    }

    /**
     * 获取远程服务器ATN结果,验证返回URL
     * @param string$notify_id 通知校验ID
     * @return string 服务器ATN结果
     * 验证结果集：
     * invalid命令参数不对 出现这个错误，请检测返回处理中partner和key是否为空
     * true 返回正确信息
     * false 请检查防火墙或者是服务器阻止端口问题以及验证时间是否超过一分钟
     */
    protected function getResponse($notify_id)
    {
        $transport = strtolower(trim('https'));
        $partner = trim($this->config['partner']);

        if($transport == 'https') {
            $veryfy_url = $this->https_verify_url;
        }
        else {
            $veryfy_url = $this->http_verify_url;
        }
        $veryfy_url = $veryfy_url."partner=" . $partner . "&notify_id=" . $notify_id;
        $responseTxt = $this->getHttpResponseGET($veryfy_url, $this->config['cacert_path']);

        return $responseTxt;
    }

    /**
     * 把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串
     * @param array $para 需要拼接的数组
     * @return string 拼接完成以后的字符串
     */
    protected function createLinkString($para)
    {
        $post_url = "";

        foreach ($para as $key => $val) {
            $post_url .= $key . '=' . $val . '&';
        }
        //去掉最后一个&字符
        //$arg = substr($arg,0,count($arg)-2);
        $post_url = rtrim($post_url, '&');

        //如果存在转义字符，那么去掉转义
        if (get_magic_quotes_gpc()) {
            $post_url = stripslashes($post_url);
        }

        return $post_url;
    }

    /**
     * 除去数组中的空值和签名参数
     * @param array $para 签名参数组
     * @return array 去掉空值与签名参数后的新签名参数组
     */
    protected function paraFilter($para)
    {
        $para_filter = array();

        foreach ($para as $key => $val) {
            if ($key == "sign" || $key == "sign_type" || $val == "") {
                continue;
            } else {
                $para_filter[$key] = $val;
            }
        }
        return $para_filter;
    }
    /**
     * 对数组排序
     * @param array $para 排序前的数组
     * @return array 排序后的数组
     */
    protected function argSort($para)
    {
        ksort($para);
        reset($para);
        return $para;
    }
    /**
     * 把数组所有元素，按照“参数=参数值”的模式用“&”字符拼接成字符串，并对字符串做urlencode编码
     * @param array $para 需要拼接的数组
     * @return string 拼接完成以后的字符串
     */
    private function createLinkStringUrlEncode($para)
    {
        $arg  = '';

        foreach ($para as $key=>$val){
            $arg .= $key ."=" .urlencode($val). "&";
        }
        //去掉最后一个&字符
        //$arg = substr($arg,0,count($arg)-2);
        $arg = rtrim($arg,'&');

        //如果存在转义字符，那么去掉转义
        if(get_magic_quotes_gpc()){$arg = stripslashes($arg);}

        return $arg;
    }


    /**
     * 远程获取数据，POST模式
     * 注意：
     * 1.使用Crul需要修改服务器中php.ini文件的设置，找到php_curl.dll去掉前面的";"就行了
     * 2.文件夹中cacert.pem是SSL证书请保证其路径有效，目前默认路径是：getcwd().'\\cacert.pem'
     * @param string $url 指定URL完整路径地址
     * @param string $cacert_url 指定当前工作目录绝对路径
     * @param array $para 请求的数据
     * @param string $input_charset 编码格式。默认值：空值
     * @return string 远程输出的数据
     */
    protected function getHttpResponsePost($url, $cacert_url, $para, $input_charset = '')
    {

        if (trim($input_charset) != '') {
            $url = $url."_input_charset=".$input_charset;
        }
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO,$cacert_url);//证书地址
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl,CURLOPT_POST,true); // post传输数据
        curl_setopt($curl,CURLOPT_POSTFIELDS,$para);// post传输数据
        $responseText = curl_exec($curl);
        if(curl_errno($curl)){
            return 'Curl error: ' . curl_error($curl);
        }
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常，可打开此开关，以便查看异常内容
        curl_close($curl);

        return $responseText;
    }
    /**
     * 远程获取数据，GET模式
     * 注意：
     * 1.使用Crul需要修改服务器中php.ini文件的设置，找到php_curl.dll去掉前面的";"就行了
     * 2.文件夹中cacert.pem是SSL证书请保证其路径有效，目前默认路径是：getcwd().'\\cacert.pem'
     * @param string $url 指定URL完整路径地址
     * @param string $cacert_url 指定当前工作目录绝对路径
     * @return string 远程输出的数据
     */
    protected function getHttpResponseGet($url,$cacert_url = null)
    {
        if(empty($cacert_url)){
            $cacert_url = $this->config['cacert_path'];
        }
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO,$cacert_url);//证书地址
        $responseText = curl_exec($curl);
        if(curl_errno($curl)){
            return 'Curl error: ' . curl_error($curl);
        }
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常，可打开此开关，以便查看异常内容
        curl_close($curl);

        return $responseText;
    }

    /**
     * RSA验签
     * @param string $data 待签名数据
     * @param string $ali_public_key_path 支付宝的公钥文件路径
     * @param string $sign 要校对的的签名结果
     * @return bool 验证结果
     */
    protected function rsaVerify($data, $ali_public_key_path, $sign)
    {
        $pubKey = file_get_contents($ali_public_key_path);
        $res = openssl_get_publickey($pubKey);
        $result = (bool)openssl_verify($data, base64_decode($sign), $res);
        openssl_free_key($res);
        return $result;
    }

    /**
     * RSA解密
     * @param string $content 需要解密的内容，密文
     * @param string $private_key_path 商户私钥文件路径
     * @return string 解密后内容，明文
     */
    public function rsaDecrypt($content, $private_key_path)
    {
        $priKey = file_get_contents($private_key_path);
        $res = openssl_get_privatekey($priKey);
        //用base64将内容还原成二进制
        $content = base64_decode($content);
        //把需要解密的内容，按128位拆开解密
        $result  = '';
        for($i = 0; $i < strlen($content)/128; $i++  ) {
            $data = substr($content, $i * 128, 128);
            openssl_private_decrypt($data, $decrypt, $res);
            $result .= $decrypt;
        }
        openssl_free_key($res);
        return $result;
    }
}